using GLib;
using Posix;
using TinyC;

public class AutoTest : GLib.Object {

    private string script_name;
    private string? directory = null;
    private string current_dir = null;
    private Logger logger;
    private SerialPortClass[] ports;
    private Cancellable cancellable;
    private static int read_sleep = 50000;

    private delegate bool TestEntry(AutoTest self);

    public AutoTest(string[] ports_name, string script_name, string? directory)
    {
        foreach(string port_name in ports_name)
        {
            var port = new SerialPortClass(port_name);

            if(!port.open())
            {
                exit(1);
            }

            this.ports += port;
        }

        this.cancellable = new Cancellable();

        this.script_name = script_name;

        this.directory = directory;

        this.logger = new Logger(script_name, directory);
    }

    ~AutoTest()
    {
        foreach(SerialPortClass port in this.ports)
        {
            port.close();
        }
    }

    private static Symbol[] symbols()
    {
        Symbol[] s = {};

        s += Backend.symbol("auto_test_read_line", (void *)AutoTest.read_line);

        s += Backend.symbol("auto_test_write_line", (void *)AutoTest.write_line);

        s += Backend.symbol("auto_test_write_data", (void *)AutoTest.write_data);

        s += Backend.symbol("auto_test_wait_line", (void *)AutoTest.wait_line);

        s += Backend.symbol("auto_test_read_data_to_file_transmode", (void *)AutoTest.read_data_to_file_transmode);

        s += Backend.symbol("auto_test_serialprot_config_set", (void *)AutoTest.serialport_config_set);

        s += Backend.symbol("auto_test_serialport_flush", (void *)AutoTest.serialport_flush);

        s += Backend.symbol("auto_test_serialport_name", (void *)AutoTest.serialport_name);

        s += Backend.symbol("auto_test_compare_file", (void *)AutoTest.compare_file);

        s += Backend.symbol("auto_test_log", (void *)AutoTest.log);

        return s;
    }

    public string? read_line(uint index)
    {
        string line = null;

        Thread.usleep(read_sleep);

        var timer = new Timer();

        do
        {
            line = this.ports[index].read_line(this.cancellable);

            if(timer.elapsed(null) > 2)
            {
                break;
            }
        }
        while(null == line);

        if(null != line)
        {
            var name = this.ports[index].name;

            this.logger.write(@"$name:<===\n\t$line\n");
        }

        return line;
    }

    public string? wait_line(uint index, string goal, int timeout, string? error_line)
    {
        double elapsed = 0;
        string line = null;
        Regex regex = null;
        string template = @"Wating for : \"$goal\"";

        try
        {
            regex = new Regex(goal, RegexCompileFlags.CASELESS);
        }
        catch(RegexError error)
        {
            print("Regex new fail, %s\n", error.message);
            return null;
        }

        if(timeout < 2)
        {
            timeout = 2;
        }

        var timer = new Timer();

        var name = this.ports[index].name;

        this.logger.write(@"$name:<===\n");

        do
        {
            Thread.usleep(read_sleep);

            line = this.ports[index].read_line(this.cancellable);

            if(null != line)
            {
                this.logger.write(@"\t$line\n");

                line = line.strip();

                if(regex.match(line))
                {
                    return line;
                }
                else if(null != error_line
                        && error_line.strip() == line)
                {
                    return null;
                }
            }

            elapsed = timer.elapsed(null);

            if(elapsed > timeout)
            {
                break;
            }
        }
        while(true);

        this.logger.write(@"$template Failed!\n");

        return null;
    }

    public bool read_data_to_file_transmode(uint index, string filename)
    {
        string path = null;
        uint bytes_written = 0;

        if(!Path.is_absolute(filename))
        {
            path = Path.build_path(Path.DIR_SEPARATOR_S, this.current_dir, filename);
        }
        else
        {
            path = filename;
        }

        var file = File.new_for_path(path);

        if(file.query_exists())
        {
            try
            {
                file.@delete();
            }
            catch(Error error)
            {
                print("delete file fail, %s\n", error.message);
            }
        }

        print("\n");

        try
        {
            var os = file.create(FileCreateFlags.NONE);

            var @is = this.ports[index].@is;

            do
            {
                Thread.usleep(read_sleep);

                try
                {
                    var bytes = @is.read_bytes(1024);

                    if(0 == bytes.length && this.ports[index].is_dsr_unset())
                    {
                        print("\n");
                        this.logger.write(@"<===\n\tWrite data to $path $bytes_written Bytes\n");
                        return true;
                    }
                    else
                    {

                        bytes_written += (uint)os.write_bytes(bytes);
                        print("\r\tReviving %u bytes...", bytes_written);
                    }
                }
                catch(Error error)
                {
                    print("io error, %s\n", error.message);
                }
            }
            while(true);
        }
        catch(Error error)
        {
            print("create file fail, %s\n", error.message);
        }

        return false;
    }

    public bool write_line(uint index, string line, ...)
    {
        var args = va_list();
        var temp = line.vprintf(args);

        if(this.ports[index].write_line(temp))
        {
            var name = this.ports[index].name;

            this.logger.write(@"$name:===>\n\t$temp\n");

            return true;
        }

        return false;
    }

    public bool write_data(uint index, uint8[] data)
    {
        if(this.ports[index].write_data(data))
        {
            var name = this.ports[index].name;
            var tmp =(string)data;
            this.logger.write_data(@"$name:===>\n\t$tmp\n".data);
            return true;
        }

        return false;
    }

    public void serialport_config_set(uint index, string option, int @value)
    {
        this.ports[index].config_set(option, @value);
    }

    public string serialport_name(uint index)
    {
        return this.ports[index].name;
    }

    public bool serialport_flush(uint index)
    {
        return this.ports[index].flush();
    }

    public bool compare_file(string file1, string file2)
    {
        uint8 fbuf[1024];

        size_t size;

        string path = null;

        Checksum checksum = new Checksum (ChecksumType.MD5);

        if(!Path.is_absolute(file1))
        {
            path = Path.build_path(Path.DIR_SEPARATOR_S, this.current_dir, file1);
        }
        else
        {
            path = file1;
        }

        FileStream stream = FileStream.open (path, "rb");

        while ((size = stream.read (fbuf)) > 0)
        {
            checksum.update (fbuf, size);
        }

        unowned string digest1 = checksum.get_string ();

        Checksum checksum2 = new Checksum (ChecksumType.MD5);

        if(!Path.is_absolute(file2))
        {
            path = Path.build_path(Path.DIR_SEPARATOR_S, this.current_dir, file2);
        }
        else
        {
            path = file2;
        }

        FileStream stream2 = FileStream.open (path, "rb");

        while ((size = stream2.read (fbuf)) > 0)
        {
            checksum2.update (fbuf, size);
        }

        unowned string digest2 = checksum2.get_string ();

        if(digest1 == digest2)
        {
            return true;
        }

        return false;
    }

    public void log(string message, ...)
    {
        var time_val = TimeVal();

        var millisecond = time_val.tv_sec * 1000 + time_val.tv_usec / 1000;

        var args = va_list();

        this.logger.write(@"Log[$millisecond]:===>\n\t" + message.vprintf(args));
    }

    public void run()
    {
        var backend = new Backend();

        current_dir = Path.get_dirname(this.script_name);

        if(true == backend.compile_file(this.script_name))
        {
            backend.import_symbols(AutoTest.symbols());

            unowned TestEntry entry = (TestEntry)backend.export_symbol("auto_test_entry");

            var res = entry(this);

            if(res)
            {
                logger.set_category(LogCategary.SUCCESS);
            }
            else
            {
                logger.set_category(LogCategary.FAIL);
            }
        }
    }
}
